/*
 * This file is part of ELCube.
 *
 * ELCube is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * ELCube is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with ELCube.  If not, see <https://www.gnu.org/licenses/>.
 */
package cn.nkpro.elcube.components.financial.services

import cn.nkpro.elcube.financial.*
import cn.nkpro.elcube.utils.DateTimeUtilz
import com.alibaba.fastjson.JSONObject
import org.springframework.stereotype.Component

import java.util.stream.Collectors

/**
 * 等额本息（年金）计算方法
 */
@Component("NkFinanceMethod01")
class NkFinanceMethod01 extends NkAbstractFinanceMethod{

    @Override
    List<ProgramDef> programDefs() {
        return Arrays.asList(
                new ProgramDef("DELAY",         "延期",        true,true,true,false,false),
                new ProgramDef("FIXED",         "固定金额",     true,true,true,false,false),
                new ProgramDef("EXTEND",        "展期",        false,false,false,false,true),
                new ProgramDef("RATE",          "调整利率",     false,false,false,true,false),
                new ProgramDef("ExtendByPercent","按贷款比例",  false,true,false,true,false),
        )
    }

    @Override
    Object apply(Object json) throws Exception {

        NkFinanceMethodOption option  = new JSONObject(json as Map<String, Object>).toJavaObject(NkFinanceMethodOption.class)

        if (option.validate()){

            List<NkFinancePaymentI> payments = calcPmt(new ArrayList<>(), option.dayOfPay, option.dateOfInterest, 0, option.dt, option.ip,option.np, option.pv, option.fv, option.tp, option.step, option.ir, option.isNextMonth)

            // 开始处理自定义规则
            if(option.customPayAmountRule!=null){

                NkFinanceRate rate = new NkFinanceRate()
                rate.activeDate = 0
                rate.rate = option.ip
                option.rates.add(rate)

                // 循环处理自定义规则，注意，规则必须按激活时间顺序执行
                for(NkFinanceCustomRule ruleLoop in option.customPayAmountRule.findAll { r->r.activeDate}){
                    switch (ruleLoop.program){
                        case 'DELAY':
                        case 'FIXED':
                            execFixedRule(payments, option, ruleLoop.activeDate)
                            break
                        case 'EXTEND':
                            execExtend   (payments, option, ruleLoop)
                            break
                        case 'RATE':
                            execRate     (payments, option, ruleLoop)
                            break
                        case 'ExtendByPercent':
                            execExtendByPercent(payments, option, ruleLoop)
                            break
                    }
                }
            }

            clearLast(payments,option.fv)
            scale(payments, option.payPrecision, option.payRoundingMode)

            return payments
        }

        return null
    }

    private void execRate(List<NkFinancePaymentI> payments, NkFinanceMethodOption option, NkFinanceCustomRule ruleLoop){

        // 生成利率表
        NkFinanceRate rate = new NkFinanceRate()
        rate.activeDate = ruleLoop.activeDate
        rate.rate = (ruleLoop.rate?:0) / option.pf //期间利率 = 年利率 / 付款频率
        option.rates.add(rate)

        // 调息日后的（含）第一个还款日期
        NkFinancePaymentI next = payments.find{ payment -> payment.expireDate >= ruleLoop.activeDate }
        if(next){
            // 如果第一个还款日不是调息日，说明该期次需要分段计算利息
            if(next.expireDate != ruleLoop.activeDate){
                // 获取前一期的到期日期
                def pres = payments.findAll { payment -> payment.expireDate < ruleLoop.activeDate }
                NkFinancePaymentI prev = pres.isEmpty()?null:pres.last()

                next.interest = calcInterest(option.rates,prev?prev.expireDate:option.dt,next.expireDate,prev?prev.residual:option.pv)
                next.pay = doubleValue(next.interest+next.principal)
            }

            // 重新计算利息
            execFixedRule(payments,option,ruleLoop.activeDate)
        }
        option.ip = rate.rate
    }

    /**
     * 展期
     * @param payments
     * @param option
     * @param ruleLoop
     */
    private void execExtend(List<NkFinancePaymentI> payments, NkFinanceMethodOption option, NkFinanceCustomRule ruleLoop){
        payments.removeIf({ payment -> payment.expireDate >= ruleLoop.activeDate })

        if(payments.isEmpty()){
            calcPmt(payments, option.dayOfPay, option.dateOfInterest, 0, option.dt, option.ip, ruleLoop.value, option.pv, option.fv, option.tp, option.step, option.ir, option.isNextMonth)
        }else{
            calcPmt(payments, option.dayOfPay, option.dateOfInterest, payments.last().period,payments.last().expireDate,option.ip,ruleLoop.value,
                    payments.last().residual,option.fv,option.tp,option.step, option.ir,option.isNextMonth)
        }
        execFixedRule(payments,option,ruleLoop.activeDate)
    }

    private void execExtendByPercent(List<NkFinancePaymentI> payments, NkFinanceMethodOption option, NkFinanceCustomRule ruleLoop){
        payments.removeIf({ payment -> payment.period >= ruleLoop.effectiveFrom })
        NkFinancePaymentI last = payments.isEmpty()?null:payments.last()

        int  from = last?.period?:0
        double pv = last?.residual?:option.pv
        int    np = ruleLoop.effectiveTo-ruleLoop.effectiveFrom+1
        double fv = option.fv + pv - (option.pv-option.fv) * ruleLoop.rate
        calcPmt(payments,
                option.dayOfPay,
                option.dateOfInterest,
                from,
                last?.expireDate?:option.dt,
                option.ip,
                np,
                pv,
                fv,
                option.tp,
                option.step,
                option.ir,
                option.isNextMonth
        )
        last = payments.last()
        calcPmt(payments,
                option.dayOfPay,
                option.dateOfInterest,
                last.period,
                last.expireDate,
                option.ip,
                option.np - last.period,
                fv,
                option.fv,
                option.tp,
                option.step,
                option.ir,
                option.isNextMonth
        )
    }

    /**
     * 设置固定还款金额
     * @param payments 还款计划
     * @param rules    变更规则集
     * @param ip       期间利率
     * @param tp       期初期末
     * @param ruleLoop 当前遍历的规则
     */
    private void execFixedRule(List<NkFinancePaymentI> payments, NkFinanceMethodOption option, long activeDate){

        List<NkFinancePaymentI> paymentsSubset = payments.findAll { payment -> payment.expireDate < activeDate }
        NkFinancePaymentI prev = paymentsSubset.isEmpty()?null:paymentsSubset.last()
        long fromDate = prev?.expireDate?:option.dt

        Double X = 0
        double minX = Double.MAX_VALUE
        int retry = 0

        /**
         * 需要重算的还款计划子集
         */
        paymentsSubset = payments.findAll{ payment -> payment.expireDate >= activeDate }

        // 如果没有找到规则激活所匹配的日期，则跳过计算
        if(paymentsSubset.isEmpty()){
            return
        }

        /**
         * 计算剩余本金
         */
        NkFinancePaymentI paymentFirst = paymentsSubset.get(0)
        Double pv = doubleValue(paymentFirst.residual + paymentFirst.principal)

        /**
         * 命中的特殊规则
         */
        List<NkFinanceCustomRule> rulesSubset = option.customPayAmountRule.stream()
                .filter({r->r.program == 'FIXED' || r.program == 'DELAY'})
                .filter({r->
                    r.activeDate <= activeDate &&
                            ((r.effectiveDate && r.effectiveDate.length>=2) || (r.effectiveFrom && r.effectiveTo))
                }).collect(Collectors.toList())

        while(true){
            retry ++

            // 上期剩余本金
            Double prevResidual = pv
            // 正常还款的期数
            int count = 0
            // 上期结余利息（即还款金额小于利息金额的部分）
            double prevOpenedInterest = 0d
            // 上期到期日期
            long   prevExpireDate = fromDate

            paymentsSubset.forEach({i->
                List<NkFinanceCustomRule> hits = rulesSubset.findAll({r->
                    (r.effectiveDate && r.effectiveDate.length>=2 && i.expireDate >= r.effectiveDate[0] && i.expireDate <= r.effectiveDate[1]) ||
                            (i.period >= r.effectiveFrom && i.period <= r.effectiveTo)
                })

                if(i.period == 1 && option.tp == 1 ){
                    // 起初支付的第一期
                    i.interest = 0
                }else{
                    // 非第一期，或期末支付
                    i.interest = doubleValue(
                            calcInterest(option.rates,prevExpireDate,i.expireDate,prevResidual + prevOpenedInterest) +
                                    prevOpenedInterest
                    )
                }
                // 计算期初利息
                if(i.period == 1 && option.dateOfInterest > option.dt){
                    long dayCount = DateTimeUtilz.dateDiff(i.expireDate,prevExpireDate)
                    i.interest = doubleValue(calcInterest(option.rates,option.dateOfInterest,prevExpireDate,dayCount,prevResidual)+i.interest)
                }

                prevOpenedInterest = 0d
                if(!hits.isEmpty()){
                    NkFinanceCustomRule rule = hits.last()
                    if(i.interest > rule.pay) {
                        // 延期支付允许将利息叠加到下一期
                        if (rule.program == 'DELAY') {
                            // 如果是最后一期
                            if (payments.indexOf(i) == payments.size() - 1) {
                                i.pay = i.interest
                            } else {
                                prevOpenedInterest = doubleValue(i.interest - rule.pay?:0)
                                i.interest = rule.pay?:0
                                i.pay = rule.pay?:0
                            }
                        }else{
                            i.pay = i.interest
                        }
                    }else{
                        i.pay = rule.pay?:0
                    }
                }else{
                    // 支付金额不能少于利息
                    i.pay = doubleValue(i.pay + X)
                    count ++
                }
                i.principal = doubleValue(i.pay-i.interest)
//                本金永远调不平
//                if(i.principal<0){
//                    i.pay = i.interest
//                    i.principal = 0
//                }
                i.residual  = doubleValue(prevResidual-i.principal)

                prevResidual   = i.residual
                prevExpireDate = i.expireDate
            })

            println "计算" + retry
            println "X=" + X
            println "prevResidual=" + prevResidual

            if(minX == Math.abs(prevResidual))
                break

            X = prevResidual/count
            minX = Math.min(minX,Math.abs(prevResidual))

            // 最多重试50次
            // 如果最后一期的剩余本金（单位分）【小于等于】等额期次数量 的一半 ！则无法继续缩小误差，退出计算
            // count/2 等额期次数量 的一半
            // * 100   将单位元转化为单位分
            if(retry >= 50 || Math.abs(prevResidual * 100) <= count / 2)
                break
        }
    }
}
